<?php
/**
 * 扩展ActiveRecord
 * <br>
 * 使用cascade方法设置级联配置，示例如下：
 * <pre>
 * class Member extends CActiveRecordAdv {
 *  ...
 *  function relations() {
 *   return array(
 *    // 用户详细资料，一对一关系
 *    'profile' => array(self::HAS_ONE, 'MemberProfile', 'member_id'),
 *   );
 *  }
 *  ...
 *  function cascade() {
 *   return array(
 *    // 启用profile的级联：setAttributes，save，validate，delete
 *    'profile'
 *   );
 *  }
 *  ...
 * }
 * </pre>
 * @author yangdongqi<yangdongqi@hayzone.com>
 * @package lib.models.ar.adv
 */
class CActiveRecordAdv extends CActiveRecord {
	
	/**
	 * Metadate config
	 * @var CARMDC
	 */
	private $_mdc = null;
	
	/**
	 * 关联FORM对象
	 * @var array
	 */
	protected $_relates = array();
	
	/**
	 * Get metadata config
	 * @return CARMDC
	 */
	public function getMdc() {
		if(!$this->_mdc) {
			$className = $this->mdcClass;
			if(strpos($className, '.') !== false) {
				Yii::import($className);
			}
			$className = explode('.', $className);
			$className = $className[count($className)-1];
			$this->_mdc = new $className($this);
		}
		return $this->_mdc;
	}
	
	public function getMdcClass() {
		return 'application.models.metadata.'.get_class($this).'Metadata';
	}
	
	/**
	 * setMdc
	 * @param CARMDC $mdc
	 * @return void
	 */
	public function setMdc(CARMDC $mdc) {
		$this->_mdc = $mdc;
	}
	
	/**
	 * @return string the associated database table name
	 */
	public function tableName() {
		return $this->mdc->tableName();
	}

	/**
	 * @return array validation rules for model attributes.
	 */
	public function rules()
	{
		return array_merge($this->mdc->rules(), array(
		));
	}

	/**
	 * @return array relational rules.
	 */
	public function relations()
	{
		return $this->mdc->relations();
	}
	
	/**
	 * 级联配置
	 * @return array
	 */
	public function cascade() {
		return array();
	}

	/**
	 * @return array customized attribute labels (name=>label)
	 */
	public function attributeLabels()
	{
		return $this->mdc->attributeLabels();
	}
	
	/**
	 * 转换标准$_FILES
	 * @see CModel::beforeValidate()
	 */
	protected function beforeValidate() {
		return parent::beforeValidate();
	}
	
	protected function normalizeFILESInner($parentModel, $nodePath, $data, &$store, $fileNode) {
		if(!is_array($data)) $data = array($data);
		foreach($data as $k=>$v) {
			if(is_array($v)) {
				if(!is_numeric($k)) {
					if(!($k[0] == 'N' && $k[1] == '-')) {
						$nodePath[] = $k;
					}
				}
				$this->normalizeFILESInner($parentModel, $nodePath, $data[$k], $store, $fileNode);
			} else {
				$className = get_class($parentModel);
				$model = $parentModel;
				$hasOne = null;
				foreach($nodePath as $one) {
					$r = $model->getMetadata()->relations[$one];
					$className = $model->getMetadata()->relations[$one]->className;
					if($hasOne === null)
						$hasOne = $r instanceof CHasOneRelation || $r instanceof CBelongsToRelation;
					$model = new $className;
				}
				if($hasOne === null || $hasOne) {
					$store[$className][$fileNode][$k] = $v;
				} else {
					$store[$className][$fileNode][][$k] = $v;
				}
				
			}
		}
	}
	
	/**
	 * 有关CActiveRecordAdv树形结构的$_FILES转化为标准的$_FILES
	 * @return void
	 */
	protected function normalizeFILES() {
		if($_FILES && !isset($_FILES['_is_locked_'])) {
			$className = get_class($this);
			$store = array();
			if(isset($_FILES[$className])) {
				foreach($_FILES[$className] as $k=>$v) {
					foreach($v as $k1=>$v1) {
						if(is_array($v1)) {
							$this->normalizeFILESInner($this, array($k1), $v1, $store, $k);
						} else {
							$store[$className][$k] = $v;
						}
					}
				}
			}
			$_FILES = $store;
			$_FILES['_is_locked_'] = true;
		}
		
//		if($_FILES && !isset($_FILES['_is_locked_'])) {
//			$relations = $this->relations();
//			$array = array();
//			foreach($_FILES as $k1 => $v1) {
//				foreach($v1 as $k2 => $v2) {
//					foreach($v2 as $k3 => $v3) {
//						if(is_array($v3) && isset($relations[$k3])) {
//							$array[$relations[$k3][1]][$k2] = $v3;
//						} else {
//							$array[$k1][$k2][$k3] = $v3;
//						}
//					}
//				}
//			}
//			$_FILES = $array;
//			# 防止递归遍历，浪费资源
//			$_FILES['_is_locked_'] = true;
//		}
//
//		CVarDumper::dump($_FILES, 111, true);die;
		
	}
	
	/**
	 * 设置模型的值，包括关联模型
	 * @see CModel::setAttributes()
	 * @return void
	 */
	public function setAttributes($values, $safeOnly = true) {
		if(!is_array($values)) return;
		$this->normalizeFILES();
		$attributes=array_flip($safeOnly ? $this->getSafeAttributeNames() : $this->attributeNames());
		
		foreach($values as $name=>$value) {
			if(isset($attributes[$name])) {
				$this->$name=$value;
			} else if(isset($this->relates[$name])) {
				$this->relates[$name]->setAttributes($value, $safeOnly);
			} else if($safeOnly) {
				$this->onUnsafeAttribute($name,$value);
			}
		}
	}
	
	/**
	 * 关联模型验证
	 * @see CModel::afterValidate()
	 * @return void
	 */
	protected function afterValidate() {
		foreach($this->relates as $name => $relate) {
			if(!$relate->validate(null, false)) {
				$this->addError($name, 
					$this->getAttributeLabel($name) . ' 未通过验证.');
			}
		}
		
		parent::afterValidate();
	}
	
	protected function beforeSave() {
		if(parent::beforeSave()) {
			foreach($this->relates as $relate) {
				if(!($relate instanceof CFormRelationBelongsTo)) continue;
				$relate->save();
			}			
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * 关联模型保存
	 * @see CActiveRecord::afterSave()
	 * @return void
	 */
	protected function afterSave() {
		parent::afterSave();
		foreach($this->relates as $relate) {
			if($relate instanceof CFormRelationBelongsTo) continue;
			$relate->save();
		}
	}
	
	protected function afterDelete() {
		parent::afterDelete();
		foreach($this->relates as $relate) {
			$relate->delete();
		}
	}
	
	/**
	 * 获取关联Form对象
	 * @return array 
	 */
	protected function getRelates() {
		if(!$this->_relates) {
			$relationSave = $this->cascade();
			$relations = $this->relations();
			foreach($relationSave as $name => $config) {
				if(!is_array($config)) {
					$name = $config;
				}
				$r = $relations[$name][0];
				$ins = $this->_relates[$name] = CFormRelation::getInstance($this, $r, $name);
				if(!is_array($config)) continue;
				foreach($config as $k=>$v) {
					$ins->$k = $v;
				}
			}
		}
		return $this->_relates;
	}

}

/**
 * CFormRelation
 * @author yangdongqi <yangdongqi@hayzone.com>
 * @package lib.models.ar
 * @property model CActiveRecord
 * @property name string
 * @property relation CActiveRelation
 * @property type string
 * @property isAttributesSet boolean
 */
class CFormRelation extends CComponent {
	
	/**
	 * 关联的名字
	 * @var string
	 */
	protected $_name;
	
	/**
	 * parent model
	 * @var CActiveRecord
	 */
	protected $_model;
	
	/**
	 * metadata
	 * @var CActiveRelation
	 */
	protected $_relation = null;
	
	/**
	 * 是否级联删除
	 * @var boolean
	 */
	public $cascadeDelete = true;
	
	/**
	 * 类型
	 * @var string
	 */
	protected $_type = null;
	
	/**
	 * 是否设置过属性
	 * 设置过属性，则需要保存
	 * @var boolean
	 */
	protected $_isAttributesSet = false;
	
	/**
	 * 类对应
	 * @var array
	 */
	protected static $_classes = array(
		CActiveRecord::HAS_ONE => 'CFormRelationHasOne',
		CActiveRecord::HAS_MANY => 'CFormRelationHasMany',
		CActiveRecord::MANY_MANY => 'CFormRelationManyMany',
		CActiveRecord::BELONGS_TO => 'CFormRelationBelongsTo',
	);
	
	/**
	 * 获取实例
	 * @param CActiveRecord $model model 名
	 * @param string $type 关联类型
	 * @param string $name 关联名称
	 */
	static function getInstance($model, $type, $name) {
		return new self::$_classes[$type]($model, $name);
	}
	
	/**
	 * __construct
	 * @param CActiveRecord $model
	 * @param string $name
	 */
	public function __construct($model, $name) {
		$this->_model = $model;
		$this->_name = $name;
	}
	
	/**
	 * getRelation
	 * @return CActiveRelation
	 */
	public function getRelation() {
		if(!$this->_relation) {
			$this->_relation = $this->_model->getMetaData()->relations[$this->_name];
		}
		return $this->_relation;
	}
	
	/**
	 * setAttributes
	 * @param array $value
	 * @param boolean $safeOnly
	 * @return void
	 */
	public function setAttributes($value, $safeOnly = true) {
		$this->_isAttributesSet = true;
		return true;
	}
	
	public function getIsAttributesSet() {
		return $this->_isAttributesSet;
	}
	
	/**
	 * validate
	 * @param array $attributes
	 * @param boolean $clearErrors
	 * @return void
	 */
	public function validate($attributes = null, $clearErrors = true) {
		return true;
	}
	
	/**
	 * delete
	 * @return boolean
	 */
	public function delete() {
		return true;
	}
	
	/**
	 * save
	 * @return boolean
	 */
	public function save() {
		return true;
	}
	
	/**
	 * getName
	 * @return string
	 */
	public function getName() {
		return $this->_name;
	}
	
	/**
	 * getModel
	 * @return CActiveRecord
	 */
	public function getModel() {
		return $this->_model;
	}
	
	/**
	 * getType
	 * @return string
	 */
	public function getType() {
		return $this->_type;
	}
	
}

/**
 * CFormRelationHasOne
 * @author yangdongqi <yangdongqi@hayzone.com>
 * @package lib.models.ar
 */
class CFormRelationHasOne extends CFormRelation {
	
	protected $_type = CActiveRecord::HAS_ONE;
	
	public function setAttributes($value, $safeOnly = true) {
		parent::setAttributes($value, $safeOnly);
		$rClass = $this->getRelation()->className;
		$rM = new $rClass;
		$model = $this->model->getRelated($this->name, true);
		if(!$model) {
			$model = $rM->populateRecord($value);
			$model->setScenario('insert');
			$model->setIsNewRecord(true);
		}
		$model->setAttributes($value);
		$this->model->{$this->name} = $model;
	}
	
	public function validate($attributes = null, $clearErrors = true) {
		if(!$this->isAttributesSet) return true;
		$model = $this->model->getRelated($this->name);
		return $model ? $model->validate($attributes, $clearErrors) : true;
	}
	
	public function save($runValidation = true, $attributes = null) {
		if(!$this->isAttributesSet) return true;
		$model = $this->model->getRelated($this->name);
		$isSuccess = true;
		if($model) {
			$model->{$this->getRelation()->foreignKey} = $this->model->primaryKey;
			$isSuccess = $model->save($runValidation, $attributes);
		}
		return $isSuccess;
	}
	
	public function delete() {
		if(!$this->cascadeDelete)
			return true;
		if($this->model->{$this->name}) {
			if(!$this->model->{$this->name}->isNewRecord)
				return $this->model->{$this->name}->delete();
		}
		return true;
	}
	
}


/**
 * CFormRelationBelongsTo
 * @author yangdongqi <yangdongqi@hayzone.com>
 * @package lib.models.ar
 */
class CFormRelationBelongsTo extends CFormRelationHasOne {
	protected $_type = CActiveRecord::BELONGS_TO;

	public function save($runValidation = true, $attributes = null) {
		if(!$this->isAttributesSet) return true;
		$model = $this->model->getRelated($this->name);
		$isSuccess = true;
		if($model) {
			if(($isSuccess = $model->save($runValidation, $attributes)) !== false) {
				$foreignKey = $this->getRelation()->foreignKey;
				$this->model->$foreignKey = $model->primaryKey;
			}
		}
		return $isSuccess;
	}
	
	public function delete() {
		return true;
	}
}




/**
 * CFormRelationHasMany
 * @author yangdongqi <yangdongqi@hayzone.com>
 * @package lib.models.ar
 */
class CFormRelationHasMany extends CFormRelation {
	
	protected $_type = CActiveRecord::HAS_MANY;
	
	public function setAttributes($values, $safeOnly = true) {
		
		parent::setAttributes($values, $safeOnly);
		$rClass = $this->getRelation()->className;
		$models = (array)$this->model->getRelated($this->name, true);
		$updateModels = array();

		$rM = new $rClass;
		$rPkName = $rM->getMetadata()->tableSchema->primaryKey;
		
		foreach($values as $mIndex => $mAttributes) {
			$model = null;
			if(isset($mAttributes[$rPkName])) {
				$model = $rM->findByPk($mAttributes[$rPkName]);
			} elseif(isset($models[$mIndex])) {
				$model = $models[$mIndex];
			}
			if(!$model) {
				$model = $rM->populateRecord($mAttributes);
				$model->setScenario('insert');
				$model->setIsNewRecord(true);
			}
			$model->setAttributes($mAttributes);
			$updateModels[$mIndex] = $model;
			
		}
		
		$this->model->{$this->name} = $updateModels;
	}
	
	
	public function validate($attributes = null, $clearErrors = true) {
		if(!$this->isAttributesSet) return true;
		$isSuccess = true;
		foreach((array)$this->model->getRelated($this->name) as $model) {
			if(!$model->validate($attributes, $clearErrors))
				$isSuccess = false;
		}
		return $isSuccess;
	}
	
	public function save($runValidation = true, $attributes = null) {
		
		if(!$this->isAttributesSet) return true;
		$existsModels = $this->getExistsRelatedModels();
		
		$models = (array)$this->model->getRelated($this->name);
		
		$fk = $this->getRelation()->foreignKey;
		$pk = $this->model->primaryKey;
		$isSuccess = true;
		
		// 删除表单中不存在的model
		foreach($existsModels as $eIndex => $existsModel) {
			if(!isset($models[$eIndex])) {
				$existsModel->delete();
			}
		}
		
		foreach($models as $mIndex => $model) {
			$model->{$fk} = $pk;
			$isSuccess &= $model->save($runValidation, $attributes);
		}
		
		return $isSuccess;
	}
	
	
	public function getExistsRelatedModels() {
		$cloneModel = clone $this->model;
		return (array)$cloneModel->getRelated($this->name, true);
	}
	
	public function delete() {
		if(!$this->cascadeDelete)
			return true;
		
		$isSuccess = true;
		foreach((array)$this->model->{$this->name} as $model) {
			if(!$model->isNewRecord)
				$isSuccess &= $model->delete();
		}
		
		return $isSuccess;
	}
	
}

/**
 * CFormRelationManyMany
 * @author yangdongqi <yangdongqi@hayzone.com>
 * @package lib.models.ar
 */
class CFormRelationManyMany extends CFormRelationHasMany {
	
	protected $_type = CActiveRecord::MANY_MANY;
	protected $_foreignInfo = null;

	public function save($runValidation = true, $attributes = null) {
		if(!$this->isAttributesSet) return true;
		
		$existsModels = $this->getExistsRelatedModels();
		$models = $this->model->getRelated($this->name);
		$unlinkIds = array();
		
		foreach($existsModels as $eIndex => $existsModel) {
			if(!$existsModel->isNewRecord) {
				$unlinkIds[$existsModel->primaryKey] = $existsModel->primaryKey;
			}
		}
		
		$isSuccess = true;
		foreach($models as $mIndex => $model) {
			if($model->isNewRecord) $model->save();
			if(isset($unlinkIds[$model->primaryKey]))
				unset($unlinkIds[$model->primaryKey]);
			$isSuccess &= $this->link($model);
		}
		
		if(!empty($unlinkIds)) {
			$this->unlink($unlinkIds);
		}
		return $isSuccess;
	}
	
	public function validate($attributes = null, $clearErrors = true) {
		$isSuccess = true;
		foreach((array)$this->model->getRelated($this->name) as $model) {
			if($model->isNewRecord && !$model->validate($attributes, $clearErrors))
				$isSuccess = false;
		}
		return $isSuccess;
	}
	
	public function delete() {
		if(!$this->cascadeDelete)
			return true;
		return $this->unlink();
	}
	
	/**
	 * 创建m2m关系
	 * @param CActiveRecord $model
	 * @return boolean
	 */
	protected function link($model) {
		$fi = $this->getForeignInfo();
		try {
			$SQL = "
				INSERT INTO {$fi['table']} ({$fi['model_fk']}, {$fi['relation_fk']}) 
				VALUES (:fk,:rFk)
			";
			$command = $this->model->getDbConnection()->createCommand($SQL);
			$command->bindValues(array(
				":fk" => $this->model->primaryKey,
				":rFk" => $model->primaryKey,
			));
			$command->execute();
		} catch (Exception $e) {
			return false;
		}
		return true;
		
	}
	
	/**
	 * 删除m2m关系
	 * @param array $ids
	 * @return boolean
	 */
	protected function unlink($ids = array()) {
		$fi = $this->getForeignInfo();
		try {
			$SQL = "
				DELETE FROM {$fi['table']} 
				WHERE {$fi['model_fk']} = :fk 
			";
			if(!empty($ids)) {
				$SQL .= "AND {$fi['relation_fk']} IN('" . implode("','", $ids) . "')";
			} 
			
			$command = $this->model->getDbConnection()->createCommand($SQL);
			$command->bindValues(array(
				":fk" => $this->model->primaryKey,
			));
			$command->execute();
		} catch(Exception $e) {
			return false;
		}
		return true;
		
	}
	
	protected function getForeignInfo() {
		if(!$this->_foreignInfo) {
			$this->_foreignInfo = $this->parseForeignKey($this->relation->foreignKey);
		}
		return $this->_foreignInfo;
	}
	
	/**
	 * Parse foreign key into table name, model FK and relation FK
	 *
	 * @param $key
	 * @return array
	 */
	protected function parseForeignKey($key) {
		if (preg_match('/(?P<table>.*?)\((?P<model_fk>.*?),(?P<relation_fk>.*?)\)/is', $key, $matches))
		{
			return array(
				'table' => $this->model->getDbConnection()->quoteTableName(trim($matches['table'])),
				'model_fk' => $this->model->getDbConnection()->quoteColumnName(trim($matches['model_fk'])),
				'relation_fk' => $this->model->getDbConnection()->quoteColumnName(trim($matches['relation_fk'])),
			);
		}

		return null;
	}
	
	
}
?>